#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#include <openssl/rand.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#include <lua.h>
#include <lauxlib.h>

#define log(...) do { printf(__VA_ARGS__);  } while(0)
#define check0(x, ...) if(x) do { log( __VA_ARGS__); exit(1); } while(0)
#define check1(x, ...) if(!(x)) do { log( __VA_ARGS__); exit(1); } while(0)


typedef struct SSLCon{
    int socket;
    SSL *sslHandle;

}SSLCon;

void freeSSLCon(SSLCon* con)
{
	close(con->socket);
    SSL_shutdown (con->sslHandle);
    SSL_free (con->sslHandle);
}

// Establish a regular tcp connection
int tcpConnect (const char* svr, short port)
{
  struct hostent *host = gethostbyname (svr);
  int handle = socket (AF_INET, SOCK_STREAM, 0);
  check1(handle >= 0, "socket return error");
  struct sockaddr_in server;
  bzero (&server, sizeof server);
  server.sin_family = AF_INET;
  server.sin_port = htons (port);
  server.sin_addr = *((struct in_addr *) host->h_addr);

  log("connecting to %s %d\n", svr, port);
  int r = connect (handle, (struct sockaddr *) &server,
                       sizeof (struct sockaddr));
  check0(r, "connect to %s %d failed\n", svr, port);
  log("connected to %s %d %d\n", svr, port, r);
  return handle;
}

void sslConnect (SSLCon* con, SSL_CTX *sslContext,const char* host, short port)
{
    bzero(con, sizeof *con);
    con->socket = tcpConnect (host, port);

    con->sslHandle = SSL_new (sslContext);
    if (con->sslHandle == NULL) {
        ERR_print_errors_fp (stderr);
        check1(0, "SSL_new failed");
    }

    if (!SSL_set_fd (con->sslHandle, con->socket)) {
        ERR_print_errors_fp (stderr);
        check1(0, "SSL_set_fd failed");
    }

    if (SSL_connect (con->sslHandle) != 1) {
        ERR_print_errors_fp (stderr);
        check1(0, "SSL_connect failed");
    }
}

int sslRead (SSLCon* con,char *buf,int ln)
{
    int rd = 0;
    int r = 1;
    while (rd < (int)(ln) && r) {
        log("reading %d\n",ln - rd);
        r = SSL_read(con->sslHandle, buf+rd, ln - rd);
        if (r < 0) {
            ERR_print_errors_fp (stderr);
        }
        check1(r >= 0, "SSL_read error return %d errno %d msg %s", r, errno, strerror(errno));
        log("read %d bytes\n", r);
        rd += r;
    }
	return r;
    //log("read %d bytes contents:\n%.*s\n", rd, rd, buf);
}

void sslWrite (SSLCon* con, const char *text)
{
    int len = strlen(text);
    int wd = SSL_write (con->sslHandle, text, len);
    check1(wd == len, "SSL_write error. return %d errno %d msg %s", wd, errno, strerror(errno));
    log("sslWrite %d bytes\n", len);
}

static int meth_request1(lua_State *L)
{
	//args: host,port,header
	char *host = (char *)lua_tostring(L, 1);
	int port = (int)lua_tonumber(L, 2);
	char *header = (char *)lua_tostring(L, 3);
	const int bufsize = 2048;
	char *buf;
	char *finalbuf;
	char **bufList[bufsize];
	int r=1,i=0,j;
	SSL_CTX *sslContext;
	sslContext = SSL_CTX_new (SSLv23_client_method ());
	if (sslContext == NULL)
    {
		ERR_print_errors_fp (stderr);
		SSL_CTX_free (sslContext);
		return 0;
	}
	else{
		SSLCon con;
        sslConnect(&con, sslContext,host, (short)port);
        sslWrite (&con, header);
		do {
			buf = (char*)malloc(bufsize);
			bufList[i] = buf;
			i += 1;
			r = sslRead (&con,buf,bufsize);
		} while(r) ;
		finalbuf = (char*)malloc(bufsize*i);
		for(j=0; j<i; j++){
			buf = bufList[j];
			strcpy(finalbuf+(bufsize*j),buf);
			free(buf);
		}
		lua_pushstring(L, finalbuf);
		free(finalbuf);
		freeSSLCon(&con);
		SSL_CTX_free (sslContext);
		return 1;
	}	
}

/**
 * Initialize modules.
 */
int luaopen_nmgssl6(lua_State *L)
{
  /* Initialize SSL */
  luaL_Reg funcs[] = {
    {"request1",	meth_request1},
    {NULL,          NULL}
  };
  if (!SSL_library_init()) {
    lua_pushstring(L, "unable to initialize SSL library");
    lua_error(L);
  }
  OpenSSL_add_all_algorithms();
  SSL_load_error_strings();
  lua_newtable(L);
  luaL_checkversion(L);
  luaL_newlib(L,funcs);
  return 1;
}
